﻿using System;
using IdentityModel;
using IdentityServer4.EntityFramework.Options;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Reborn.IdentityServer4.Admin.Api.AuditLogging;
using Reborn.IdentityServer4.Admin.Api.Configuration;
using Reborn.IdentityServer4.Admin.Api.Configuration.ApplicationParts;
using Reborn.IdentityServer4.Admin.Api.Configuration.Constants;
using Reborn.IdentityServer4.Admin.Api.Helpers.Localization;
using Reborn.IdentityServer4.Admin.BusinessLogic.Identity.Dtos.Identity;
using Reborn.IdentityServer4.Admin.EntityFramework.Configuration.Configuration;
using Reborn.IdentityServer4.Admin.EntityFramework.Configuration.MySql;
using Reborn.IdentityServer4.Admin.EntityFramework.Configuration.PostgreSQL;
using Reborn.IdentityServer4.Admin.EntityFramework.Configuration.SqlServer;
using Reborn.IdentityServer4.Admin.EntityFramework.Helpers;
using Reborn.IdentityServer4.Admin.EntityFramework.Interfaces;
using Reborn.IdentityServer4.Admin.AuditLogging.EntityFramework.DbContexts;
using Reborn.IdentityServer4.Admin.AuditLogging.EntityFramework.Entities;
using Reborn.IdentityServer4.Admin.AuditLogging.EntityFramework.Extensions;
using Reborn.IdentityServer4.Admin.AuditLogging.EntityFramework.Repositories;
using Reborn.IdentityServer4.Admin.AuditLogging.EntityFramework.Services;

namespace Reborn.IdentityServer4.Admin.Api.Helpers;

public static class StartupHelpers
{
    public static IServiceCollection AddAuditEventLogging<TAuditLoggingDbContext, TAuditLog>(
        this IServiceCollection services, IConfiguration configuration)
        where TAuditLog : AuditLog, new()
        where TAuditLoggingDbContext : IAuditLoggingDbContext<TAuditLog>
    {
        var auditLoggingConfiguration = configuration.GetSection(nameof(AuditLoggingConfiguration))
            .Get<AuditLoggingConfiguration>();
        services.AddSingleton(auditLoggingConfiguration);

        services.AddAuditLogging(options => { options.Source = auditLoggingConfiguration.Source; })
            .AddEventData<ApiAuditSubject, ApiAuditAction>()
            .AddAuditSinks<DatabaseAuditEventLoggerSink<TAuditLog>>();

        services
            .AddTransient<IAuditLoggingRepository<TAuditLog>,
                AuditLoggingRepository<TAuditLoggingDbContext, TAuditLog>>();

        return services;
    }

    public static IServiceCollection AddAdminApiCors(this IServiceCollection services,
        AdminApiConfiguration adminApiConfiguration)
    {
        services.AddCors(options =>
        {
            options.AddDefaultPolicy(
                builder =>
                {
                    if (adminApiConfiguration.CorsAllowAnyOrigin)
                        builder.AllowAnyOrigin();
                    else
                        builder.WithOrigins(adminApiConfiguration.CorsAllowOrigins);

                    builder.AllowAnyHeader();
                    builder.AllowAnyMethod();
                });
        });

        return services;
    }

    /// <summary>
    ///     Register services for MVC
    /// </summary>
    /// <param name="services"></param>
    public static void AddMvcServices<TUserDto, TRoleDto,
        TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken,
        TUsersDto, TRolesDto, TUserRolesDto, TUserClaimsDto,
        TUserProviderDto, TUserProvidersDto, TUserChangePasswordDto, TRoleClaimsDto, TUserClaimDto, TRoleClaimDto>(
        this IServiceCollection services)
        where TUserDto : UserDto<TKey>, new()
        where TRoleDto : RoleDto<TKey>, new()
        where TUser : IdentityUser<TKey>
        where TRole : IdentityRole<TKey>
        where TKey : IEquatable<TKey>
        where TUserClaim : IdentityUserClaim<TKey>
        where TUserRole : IdentityUserRole<TKey>
        where TUserLogin : IdentityUserLogin<TKey>
        where TRoleClaim : IdentityRoleClaim<TKey>
        where TUserToken : IdentityUserToken<TKey>
        where TUsersDto : UsersDto<TUserDto, TKey>
        where TRolesDto : RolesDto<TRoleDto, TKey>
        where TUserRolesDto : UserRolesDto<TRoleDto, TKey>
        where TUserClaimsDto : UserClaimsDto<TUserClaimDto, TKey>
        where TUserProviderDto : UserProviderDto<TKey>
        where TUserProvidersDto : UserProvidersDto<TUserProviderDto, TKey>
        where TUserChangePasswordDto : UserChangePasswordDto<TKey>
        where TRoleClaimsDto : RoleClaimsDto<TRoleClaimDto, TKey>
        where TUserClaimDto : UserClaimDto<TKey>
        where TRoleClaimDto : RoleClaimDto<TKey>
    {
        services.AddLocalization(opts => { opts.ResourcesPath = ConfigurationConsts.ResourcesPath; });

        services.TryAddTransient(typeof(IGenericControllerLocalizer<>), typeof(GenericControllerLocalizer<>));

        services.AddControllersWithViews(o => { o.Conventions.Add(new GenericControllerRouteConvention()); })
            .AddDataAnnotationsLocalization()
            .ConfigureApplicationPartManager(m =>
            {
                m.FeatureProviders.Add(
                    new GenericTypeControllerFeatureProvider<TUserDto, TRoleDto,
                        TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken,
                        TUsersDto, TRolesDto, TUserRolesDto, TUserClaimsDto,
                        TUserProviderDto, TUserProvidersDto, TUserChangePasswordDto, TRoleClaimsDto, TUserClaimDto,
                        TRoleClaimDto>());
            });
    }

    /// <summary>
    ///     Register DbContexts for IdentityServer ConfigurationStore and PersistedGrants, Identity and Logging
    ///     Configure the connection strings in AppSettings.json
    /// </summary>
    /// <typeparam name="TConfigurationDbContext"></typeparam>
    /// <typeparam name="TPersistedGrantDbContext"></typeparam>
    /// <typeparam name="TLogDbContext"></typeparam>
    /// <typeparam name="TIdentityDbContext"></typeparam>
    /// <typeparam name="TAuditLoggingDbContext"></typeparam>
    /// <typeparam name="TDataProtectionDbContext"></typeparam>
    /// <param name="services"></param>
    /// <param name="configuration"></param>
    public static void AddDbContexts<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext,
        TLogDbContext, TAuditLoggingDbContext, TDataProtectionDbContext, TAuditLog>(this IServiceCollection services,
        IConfiguration configuration)
        where TIdentityDbContext : DbContext
        where TPersistedGrantDbContext : DbContext, IAdminPersistedGrantDbContext
        where TConfigurationDbContext : DbContext, IAdminConfigurationDbContext
        where TLogDbContext : DbContext, IAdminLogDbContext
        where TAuditLoggingDbContext : DbContext, IAuditLoggingDbContext<TAuditLog>
        where TDataProtectionDbContext : DbContext, IDataProtectionKeyContext
        where TAuditLog : AuditLog
    {
        var databaseProvider = configuration.GetSection(nameof(DatabaseProviderConfiguration))
            .Get<DatabaseProviderConfiguration>();
        var databaseMigrations =
            configuration.GetSection(nameof(DatabaseMigrationsConfiguration)).Get<DatabaseMigrationsConfiguration>() ??
            new DatabaseMigrationsConfiguration();
        var connectionStrings = configuration.GetSection("ConnectionStrings").Get<ConnectionStringsConfiguration>();

        switch (databaseProvider.ProviderType)
        {
            case DatabaseProviderType.SqlServer:
                services
                    .RegisterSqlServerDbContexts<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext,
                        TLogDbContext, TAuditLoggingDbContext, TDataProtectionDbContext, TAuditLog>(connectionStrings,
                        databaseMigrations);
                break;
            case DatabaseProviderType.PostgreSQL:
                services
                    .RegisterNpgSqlDbContexts<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext,
                        TLogDbContext, TAuditLoggingDbContext, TDataProtectionDbContext, TAuditLog>(connectionStrings,
                        databaseMigrations);
                break;
            case DatabaseProviderType.MySql:
                services
                    .RegisterMySqlDbContexts<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext,
                        TLogDbContext, TAuditLoggingDbContext, TDataProtectionDbContext, TAuditLog>(connectionStrings,
                        databaseMigrations);
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(databaseProvider.ProviderType),
                    $@"The value needs to be one of {string.Join(", ", Enum.GetNames(typeof(DatabaseProviderType)))}.");
        }
    }

    /// <summary>
    ///     Add authentication middleware for an API
    /// </summary>
    /// <typeparam name="TIdentityDbContext">DbContext for an access to Identity</typeparam>
    /// <typeparam name="TUser">Entity with User</typeparam>
    /// <typeparam name="TRole">Entity with Role</typeparam>
    /// <param name="services"></param>
    /// <param name="configuration"></param>
    public static void AddApiAuthentication<TIdentityDbContext, TUser, TRole>(this IServiceCollection services,
        IConfiguration configuration)
        where TIdentityDbContext : DbContext
        where TRole : class
        where TUser : class
    {
        var adminApiConfiguration =
            configuration.GetSection(nameof(AdminApiConfiguration)).Get<AdminApiConfiguration>();

        services
            .AddIdentity<TUser, TRole>(options => configuration.GetSection(nameof(IdentityOptions)).Bind(options))
            .AddEntityFrameworkStores<TIdentityDbContext>()
            .AddDefaultTokenProviders();

        services.AddAuthentication(options =>
            {
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
            {
                options.Authority = adminApiConfiguration.IdentityServerBaseUrl;
                options.RequireHttpsMetadata = adminApiConfiguration.RequireHttpsMetadata;
                options.Audience = adminApiConfiguration.OidcApiName;
            });
    }

    /// <summary>
    ///     Register in memory DbContexts for IdentityServer ConfigurationStore and PersistedGrants, Identity and Logging
    ///     For testing purpose only
    /// </summary>
    /// <typeparam name="TConfigurationDbContext"></typeparam>
    /// <typeparam name="TPersistedGrantDbContext"></typeparam>
    /// <typeparam name="TLogDbContext"></typeparam>
    /// <typeparam name="TIdentityDbContext"></typeparam>
    /// <typeparam name="TAuditLoggingDbContext"></typeparam>
    /// <typeparam name="TDataProtectionDbContext"></typeparam>
    /// <param name="services"></param>
    public static void RegisterDbContextsStaging<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext,
        TLogDbContext, TAuditLoggingDbContext, TDataProtectionDbContext>(this IServiceCollection services)
        where TIdentityDbContext : DbContext
        where TPersistedGrantDbContext : DbContext, IAdminPersistedGrantDbContext
        where TConfigurationDbContext : DbContext, IAdminConfigurationDbContext
        where TLogDbContext : DbContext, IAdminLogDbContext
        where TAuditLoggingDbContext : DbContext, IAuditLoggingDbContext<AuditLog>
        where TDataProtectionDbContext : DbContext, IDataProtectionKeyContext
    {
        var persistedGrantsDatabaseName = Guid.NewGuid().ToString();
        var configurationDatabaseName = Guid.NewGuid().ToString();
        var logDatabaseName = Guid.NewGuid().ToString();
        var identityDatabaseName = Guid.NewGuid().ToString();
        var auditLoggingDatabaseName = Guid.NewGuid().ToString();
        var dataProtectionDatabaseName = Guid.NewGuid().ToString();

        var operationalStoreOptions = new OperationalStoreOptions();
        services.AddSingleton(operationalStoreOptions);

        var storeOptions = new ConfigurationStoreOptions();
        services.AddSingleton(storeOptions);

        services.AddDbContext<TIdentityDbContext>(optionsBuilder
            => optionsBuilder.UseInMemoryDatabase(identityDatabaseName));
        services.AddDbContext<TPersistedGrantDbContext>(optionsBuilder
            => optionsBuilder.UseInMemoryDatabase(persistedGrantsDatabaseName));
        services.AddDbContext<TConfigurationDbContext>(optionsBuilder
            => optionsBuilder.UseInMemoryDatabase(configurationDatabaseName));
        services.AddDbContext<TLogDbContext>(optionsBuilder => optionsBuilder.UseInMemoryDatabase(logDatabaseName));
        services.AddDbContext<TAuditLoggingDbContext>(optionsBuilder
            => optionsBuilder.UseInMemoryDatabase(auditLoggingDatabaseName));
        services.AddDbContext<TDataProtectionDbContext>(optionsBuilder
            => optionsBuilder.UseInMemoryDatabase(dataProtectionDatabaseName));
    }

    public static void AddAuthorizationPolicies(this IServiceCollection services)
    {
        var adminApiConfiguration = services.BuildServiceProvider().GetService<AdminApiConfiguration>();

        services.AddAuthorization(options =>
        {
            options.AddPolicy(AuthorizationConsts.AdministrationPolicy,
                policy =>
                    policy.RequireAssertion(context => context.User.HasClaim(c =>
                            (c.Type == JwtClaimTypes.Role && c.Value == adminApiConfiguration.AdministrationRole) ||
                            (c.Type == $"client_{JwtClaimTypes.Role}" &&
                             c.Value == adminApiConfiguration.AdministrationRole)
                        ) && context.User.HasClaim(c
                            => c.Type == JwtClaimTypes.Scope && c.Value == adminApiConfiguration.OidcApiName)
                    ));
        });
    }

    public static void AddIdSHealthChecks<TConfigurationDbContext, TPersistedGrantDbContext, TIdentityDbContext,
        TLogDbContext, TAuditLoggingDbContext, TDataProtectionDbContext>(this IServiceCollection services,
        IConfiguration configuration, AdminApiConfiguration adminApiConfiguration)
        where TConfigurationDbContext : DbContext, IAdminConfigurationDbContext
        where TPersistedGrantDbContext : DbContext, IAdminPersistedGrantDbContext
        where TIdentityDbContext : DbContext
        where TLogDbContext : DbContext, IAdminLogDbContext
        where TAuditLoggingDbContext : DbContext, IAuditLoggingDbContext<AuditLog>
        where TDataProtectionDbContext : DbContext, IDataProtectionKeyContext
    {
        var configurationDbConnectionString =
            configuration.GetConnectionString(ConfigurationConsts.ConfigurationDbConnectionStringKey);
        var persistedGrantsDbConnectionString =
            configuration.GetConnectionString(ConfigurationConsts.PersistedGrantDbConnectionStringKey);
        var identityDbConnectionString =
            configuration.GetConnectionString(ConfigurationConsts.IdentityDbConnectionStringKey);
        var logDbConnectionString =
            configuration.GetConnectionString(ConfigurationConsts.AdminLogDbConnectionStringKey);
        var auditLogDbConnectionString =
            configuration.GetConnectionString(ConfigurationConsts.AdminAuditLogDbConnectionStringKey);
        var dataProtectionDbConnectionString =
            configuration.GetConnectionString(ConfigurationConsts.DataProtectionDbConnectionStringKey);

        var identityServerUri = adminApiConfiguration.IdentityServerBaseUrl;
        var healthChecksBuilder = services.AddHealthChecks()
            .AddDbContextCheck<TConfigurationDbContext>("ConfigurationDbContext")
            .AddDbContextCheck<TPersistedGrantDbContext>("PersistedGrantsDbContext")
            .AddDbContextCheck<TIdentityDbContext>("IdentityDbContext")
            .AddDbContextCheck<TLogDbContext>("LogDbContext")
            .AddDbContextCheck<TAuditLoggingDbContext>("AuditLogDbContext")
            .AddDbContextCheck<TDataProtectionDbContext>("DataProtectionDbContext")
            .AddIdentityServer(new Uri(identityServerUri), "Identity Server");

        var serviceProvider = services.BuildServiceProvider();
        var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
        using (var scope = scopeFactory.CreateScope())
        {
            var configurationTableName =
                DbContextHelpers.GetEntityTable<TConfigurationDbContext>(scope.ServiceProvider);
            var persistedGrantTableName =
                DbContextHelpers.GetEntityTable<TPersistedGrantDbContext>(scope.ServiceProvider);
            var identityTableName = DbContextHelpers.GetEntityTable<TIdentityDbContext>(scope.ServiceProvider);
            var logTableName = DbContextHelpers.GetEntityTable<TLogDbContext>(scope.ServiceProvider);
            var auditLogTableName = DbContextHelpers.GetEntityTable<TAuditLoggingDbContext>(scope.ServiceProvider);
            var dataProtectionTableName =
                DbContextHelpers.GetEntityTable<TDataProtectionDbContext>(scope.ServiceProvider);

            var databaseProvider = configuration.GetSection(nameof(DatabaseProviderConfiguration))
                .Get<DatabaseProviderConfiguration>();
            switch (databaseProvider.ProviderType)
            {
                case DatabaseProviderType.SqlServer:
                    healthChecksBuilder
                        .AddSqlServer(configurationDbConnectionString, name: "ConfigurationDb",
                            healthQuery: $"SELECT TOP 1 * FROM dbo.[{configurationTableName}]")
                        .AddSqlServer(persistedGrantsDbConnectionString, name: "PersistentGrantsDb",
                            healthQuery: $"SELECT TOP 1 * FROM dbo.[{persistedGrantTableName}]")
                        .AddSqlServer(identityDbConnectionString, name: "IdentityDb",
                            healthQuery: $"SELECT TOP 1 * FROM dbo.[{identityTableName}]")
                        .AddSqlServer(logDbConnectionString, name: "LogDb",
                            healthQuery: $"SELECT TOP 1 * FROM dbo.[{logTableName}]")
                        .AddSqlServer(auditLogDbConnectionString, name: "AuditLogDb",
                            healthQuery: $"SELECT TOP 1 * FROM dbo.[{auditLogTableName}]")
                        .AddSqlServer(dataProtectionDbConnectionString, name: "DataProtectionDb",
                            healthQuery: $"SELECT TOP 1 * FROM dbo.[{dataProtectionTableName}]");
                    break;
                case DatabaseProviderType.PostgreSQL:
                    healthChecksBuilder
                        .AddNpgSql(configurationDbConnectionString, name: "ConfigurationDb",
                            healthQuery: $"SELECT * FROM \"{configurationTableName}\" LIMIT 1")
                        .AddNpgSql(persistedGrantsDbConnectionString, name: "PersistentGrantsDb",
                            healthQuery: $"SELECT * FROM \"{persistedGrantTableName}\" LIMIT 1")
                        .AddNpgSql(identityDbConnectionString, name: "IdentityDb",
                            healthQuery: $"SELECT * FROM \"{identityTableName}\" LIMIT 1")
                        .AddNpgSql(logDbConnectionString, name: "LogDb",
                            healthQuery: $"SELECT * FROM \"{logTableName}\" LIMIT 1")
                        .AddNpgSql(auditLogDbConnectionString, name: "AuditLogDb",
                            healthQuery: $"SELECT * FROM \"{auditLogTableName}\"  LIMIT 1")
                        .AddNpgSql(dataProtectionDbConnectionString, name: "DataProtectionDb",
                            healthQuery: $"SELECT * FROM \"{dataProtectionTableName}\"  LIMIT 1");
                    break;
                case DatabaseProviderType.MySql:
                    healthChecksBuilder
                        .AddMySql(configurationDbConnectionString, name: "ConfigurationDb")
                        .AddMySql(persistedGrantsDbConnectionString, name: "PersistentGrantsDb")
                        .AddMySql(identityDbConnectionString, name: "IdentityDb")
                        .AddMySql(logDbConnectionString, name: "LogDb")
                        .AddMySql(auditLogDbConnectionString, name: "AuditLogDb")
                        .AddMySql(dataProtectionDbConnectionString, name: "DataProtectionDb");
                    break;
                default:
                    throw new NotImplementedException(
                        $"Health checks not defined for database provider {databaseProvider.ProviderType}");
            }
        }
    }

    public static void AddForwardHeaders(this IApplicationBuilder app)
    {
        var forwardingOptions = new ForwardedHeadersOptions
        {
            ForwardedHeaders = ForwardedHeaders.All
        };

        forwardingOptions.KnownNetworks.Clear();
        forwardingOptions.KnownProxies.Clear();

        app.UseForwardedHeaders(forwardingOptions);
    }
}